<?php 

namespace pmvc\mvc;

use pmvc\ioc\OrderUtil;
use pmvc\mvc\filter\ControllerRequestFilter;
use pmvc\mvc\ioc\ViewHandlerMappingExtension;
use pmvc\util\StringUtil;
use pmvc\util\ClassUtil;
use pmvc\log\Log;
use pmvc\ApplicationContext;
use pmvc\XmlApplicationContext;
use Exception;

class Dispatcher {

	const REDIRECT_VIEWNAME_PREFIX	= "redirect:";
	const FORWARD_VIEWNAME_PREFIX	= "forward:";
	const ERROR_VIEWNAME_PREFIX		= "errorController:";

	const FACTORY_CONTEXT			= "context";
	const FACTORY_CLASSPATH			= "classpath";
	const FACTORY_EXPAND_ARCHIVES	= "expandArchives";
	const FACTORY_EXPANSION_DIR		= "expansionDirectory";
	const FACTORY_WEBINF_DIR		= "webInfDir";

	private $applicationContext	= null;

    /**
     * Creates the Dispatcher.
     * @param ApplicationContext $applicationContext
     */
    public function __construct(ApplicationContext $applicationContext) {
    	$this->applicationContext = $applicationContext;
    }

	/**
	 * Convienience function for creating and configuring the
	 * MVC dispatcher along with an optional application context.
	 * @param Array $settings 
	 * @return Dispatcher the {@link Dispatcher}
	 */
	public static function factory(Array $settings=Array()) {

		// populate some settings from web-inf
		if (isset($settings[self::FACTORY_WEBINF_DIR])) {
			$webInf = $settings[self::FACTORY_WEBINF_DIR];
			
			// setup context
			if (!isset($settings[self::FACTORY_CONTEXT])) {
				$settings[self::FACTORY_CONTEXT] = $webInf."/applicationContext.xml";
			}
			
			// setup classpath
			if (!isset($settings[self::FACTORY_CLASSPATH])) {
				$cp = Array($webInf."/classes");
				$cp = array_merge($cp, ClassUtil::readDirectoriesAsPathElements(Array($webInf."/lib")));
				$settings[self::FACTORY_CLASSPATH] = $cp;
			}
		}
	
		// context
		$context = isset($settings[self::FACTORY_CONTEXT])
			? $settings[self::FACTORY_CONTEXT]
			: dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/applicationContext.xml";

		// classpath
		$classPath = null;
		if (isset($settings[self::FACTORY_CLASSPATH])) {
			$classPath = $settings[self::FACTORY_CLASSPATH];
		} else {
			$classPath = Array();
			
			// add the classes directory
			$classesDir = dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/classes";
			if (file_exists($classesDir) && is_dir($classesDir)) {
				$classPath []= $classesDir;
			}
	
			// add lib folders
			$libDir = dirname($_SERVER["SCRIPT_FILENAME"])."/WEB-INF/lib";
			foreach (ClassUtil::readDirectoriesAsPathElements(Array($libDir)) as $path) {
				$classPath []= $path;
			}
		}

		// classloader settings
		$expandArchives = isset($settings[self::FACTORY_EXPAND_ARCHIVES])
			? $settings[self::FACTORY_EXPAND_ARCHIVES]===true
			: true;
		$expansionDirectory = isset($settings[self::FACTORY_EXPANSION_DIR])
			? $settings[self::FACTORY_EXPANSION_DIR]===true
			: sys_get_temp_dir().DIRECTORY_SEPARATOR.md5($_SERVER["SCRIPT_FILENAME"]);

		// create the directory if needed
		if (!file_exists($expansionDirectory)) {
			@mkdir($expansionDirectory, 0755, true);
		}

		// create the class loaders
		ClassUtil::addClassLoadersForPaths(
			$classPath, $expandArchives, $expansionDirectory);
		

		// return it
		return new Dispatcher(new XmlApplicationContext($context));
	}

    /**
     * Returns the URL that was requested.
     * @return string the URL
     */
    public function getApplicationContext() {
    	return $this->applicationContext;
    }

    /**
     * Dispatches the request.
     */
    public function dispatch() {

    	// create request and response
    	$request	= new Request($this);
    	$response	= new Response($this);

    	// dispatch
    	$this->forward($request, $response);
    }

    /**
     * Dispatches a request.
     * @param Request $request the request
     * @param Response $response the response
     */
    public function forward(Request $request, Response $response) {

    	// log it
    	Log::trace(__CLASS__, "Dispatching request");
    	$dispatchStartTime = microtime(true);
 
    	// start session
    	$sessionId = session_id();
    	if (empty($sessionId) || !isset($_SESSION)) {
    		session_start();
    	}
   
    	// our chain
    	$chain = new RequestChain();

    	// get the request filters
    	$requestUrl = $request->getRequestedUrl();
    	$requestFilterMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\RequestFilterMapping");
    	$filters = Array();
    	foreach ($requestFilterMappings as $mapping) {
    		foreach ($mapping->getRequestFilters($requestUrl, $request) as $mappedObject) {
    			$request->addPathVariables($mappedObject);
    			$filters[]= $mappedObject->getObject();
    		}
    	}

    	// order them and add to the chain
    	OrderUtil::sort($filters);
    	foreach ($filters as $filter) {
    		$chain->addFilter($filter);
    	}

    	// add the controller to the end of the chain
    	$controllerFilter = new ControllerRequestFilter();
    	$chain->addFilter($controllerFilter);

    	// execute the chain
        try {
    		$chain->next($request, $response);
    	} catch(Exception $e) {

    		// forward to exception handler
    		if ($request->getAttribute("exception")) {
    			Log::fatal(__CLASS__, "Exception while showing error page: ".$e->getMessage());
    			throw $request->getAttribute("exception");
    		}
    		$request->setAttribute("exception", $e);
    		$request->setAttribute("url", $requestUrl);
    		Log::error(__CLASS__,
    			"Exception during dispatch, forwarding to: {0}",
    			self::ERROR_VIEWNAME_PREFIX.'500', $e);
    		$this->forward(Request::factory($request, Array(
	    			Request::URI  => self::ERROR_VIEWNAME_PREFIX.'500'
	    		)), $response);
    		return;
    	}

    	// print the output buffer
    	if (!$response->isCommitted()) {
	    	$response->send();
    	}

    	// log elapsed time
    	Log::trace(__CLASS__, "Dispatcher took {0} milliseconds", (microtime(true)-$dispatchStartTime)*1000);
    }

}

?>